/*
 * @path        : \threejs\src\views\tree\index.js
 * @Author      : yvangod
 * @since       : 2021-03-31 16:31:29
 * @LastAuthor  : yvangod
 * @lastTime    : 2021-04-02 10:50:09
 * @message     : 走好每一步，才能走的更快更稳
 */
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
import { GodRaysFakeSunShader, GodRaysDepthMaskShader, GodRaysCombineShader, GodRaysGenerateShader } from 'three/examples/jsm/shaders/GodRaysShader.js';
import Stats from 'three/examples/jsm/libs/stats.module.js'

import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader'
import { merge } from "lodash"
import { proxy } from "@/utils"
class Tree {
  #defaultOptions = {
    width: window.innerWidth,
    height: window.innerHeight,
    camera: {},
    renderer: {
      clearColor: 0x222222
    },
    scene: {
      background: 0xb9d3ff
    },
    postprocessing: {
      enabled: true,
      bgColor: 0x000511,
      sunColor: 0xffee00,
      orbitRadius: 200,
      godrayRenderTargetResolutionMultiplier: 1.0 / 4.0
    }
  }
  constructor(el, options) {
    if (!(el instanceof HTMLElement)) {
      throw new Error("第一个参数必须是一个HTMLElement对象");
    }

    this.el = el
    this.options = merge({}, this.#defaultOptions, options)

    this.sunPosition = new THREE.Vector3( 0, 1000, - 1000 );
    this.clipPosition = new THREE.Vector4();
    this.screenSpacePosition = new THREE.Vector3();

    this.initStat()
    this.initClock()
    this.init()
    this.initScene()
    // this.drawGridHelper()
    this.initLoader()
    this.initGeo()
    // this.initLight()
    this.initCamera()
    this.initControl()
    this.initPostprocessing(window.innerWidth, window.innerHeight)
    // this.resize()
    window.addEventListener('resize', this.resize.bind(this))
  }

  initPostprocessing(renderTargetWidth, renderTargetHeight) {
    const postprocessing = this.options.postprocessing
    const godrayRenderTargetResolutionMultiplier = postprocessing.godrayRenderTargetResolutionMultiplier;
    postprocessing.scene = new THREE.Scene();

    postprocessing.camera = new THREE.OrthographicCamera(- 0.5, 0.5, 0.5, - 0.5, - 10000, 10000);
    postprocessing.camera.position.z = 100;

    postprocessing.scene.add(postprocessing.camera);

    const pars = { minFilter: THREE.LinearFilter, magFilter: THREE.LinearFilter, format: THREE.RGBFormat };
    postprocessing.rtTextureColors = new THREE.WebGLRenderTarget(renderTargetWidth, renderTargetHeight, pars);

    // Switching the depth formats to luminance from rgb doesn't seem to work. I didn't
    // investigate further for now.
    // pars.format = LuminanceFormat;

    // I would have this quarter size and use it as one of the ping-pong render
    // targets but the aliasing causes some temporal flickering

    postprocessing.rtTextureDepth = new THREE.WebGLRenderTarget(renderTargetWidth, renderTargetHeight, pars);
    postprocessing.rtTextureDepthMask = new THREE.WebGLRenderTarget(renderTargetWidth, renderTargetHeight, pars);

    // The ping-pong render targets can use an adjusted resolution to minimize cost

    const adjustedWidth = renderTargetWidth * godrayRenderTargetResolutionMultiplier;
    const adjustedHeight = renderTargetHeight * godrayRenderTargetResolutionMultiplier;
    postprocessing.rtTextureGodRays1 = new THREE.WebGLRenderTarget(adjustedWidth, adjustedHeight, pars);
    postprocessing.rtTextureGodRays2 = new THREE.WebGLRenderTarget(adjustedWidth, adjustedHeight, pars);

    // god-ray shaders

    const godraysMaskShader = GodRaysDepthMaskShader;
    postprocessing.godrayMaskUniforms = THREE.UniformsUtils.clone(godraysMaskShader.uniforms);
    postprocessing.materialGodraysDepthMask = new THREE.ShaderMaterial({

      uniforms: postprocessing.godrayMaskUniforms,
      vertexShader: godraysMaskShader.vertexShader,
      fragmentShader: godraysMaskShader.fragmentShader

    });

    const godraysGenShader = GodRaysGenerateShader;
    postprocessing.godrayGenUniforms = THREE.UniformsUtils.clone(godraysGenShader.uniforms);
    postprocessing.materialGodraysGenerate = new THREE.ShaderMaterial({

      uniforms: postprocessing.godrayGenUniforms,
      vertexShader: godraysGenShader.vertexShader,
      fragmentShader: godraysGenShader.fragmentShader

    });

    const godraysCombineShader = GodRaysCombineShader;
    postprocessing.godrayCombineUniforms = THREE.UniformsUtils.clone(godraysCombineShader.uniforms);
    postprocessing.materialGodraysCombine = new THREE.ShaderMaterial({

      uniforms: postprocessing.godrayCombineUniforms,
      vertexShader: godraysCombineShader.vertexShader,
      fragmentShader: godraysCombineShader.fragmentShader

    });

    const godraysFakeSunShader = GodRaysFakeSunShader;
    postprocessing.godraysFakeSunUniforms = THREE.UniformsUtils.clone(godraysFakeSunShader.uniforms);
    postprocessing.materialGodraysFakeSun = new THREE.ShaderMaterial({

      uniforms: postprocessing.godraysFakeSunUniforms,
      vertexShader: godraysFakeSunShader.vertexShader,
      fragmentShader: godraysFakeSunShader.fragmentShader

    });

    postprocessing.godraysFakeSunUniforms.bgColor.value.setHex(this.options.postprocessing.bgColor);
    postprocessing.godraysFakeSunUniforms.sunColor.value.setHex(this.options.postprocessing.sunColor);

    postprocessing.godrayCombineUniforms.fGodRayIntensity.value = 0.75;

    postprocessing.quad = new THREE.Mesh(
      new THREE.PlaneGeometry(1.0, 1.0),
      postprocessing.materialGodraysGenerate
    );
    postprocessing.quad.position.z = - 9900;
    postprocessing.scene.add(postprocessing.quad);
  }

  resize() {
    const postprocessing = this.options.postprocessing
    const godrayRenderTargetResolutionMultiplier = postprocessing.godrayRenderTargetResolutionMultiplier
    const renderTargetWidth = window.innerWidth;
    const renderTargetHeight = window.innerHeight;

    this.camera.aspect = renderTargetWidth / renderTargetHeight
    this.camera.updateProjectionMatrix()
    this.renderer.setSize(renderTargetWidth, renderTargetHeight)

    postprocessing.rtTextureColors.setSize(renderTargetWidth, renderTargetHeight);
    postprocessing.rtTextureDepth.setSize(renderTargetWidth, renderTargetHeight);
    postprocessing.rtTextureDepthMask.setSize(renderTargetWidth, renderTargetHeight);

    const adjustedWidth = renderTargetWidth * godrayRenderTargetResolutionMultiplier;
    const adjustedHeight = renderTargetHeight * godrayRenderTargetResolutionMultiplier;
    postprocessing.rtTextureGodRays1.setSize(adjustedWidth, adjustedHeight);
    postprocessing.rtTextureGodRays2.setSize(adjustedWidth, adjustedHeight);
  }

  animate() {
    this.animationFrame = requestAnimationFrame(proxy(this.animate, this));
    this.controls.update(this.clock.getDelta())
    this.stats.begin()
    this.render()
    this.stats.end()
  }

  render() {
    const renderer = this.renderer
    const scene = this.scene
    const camera = this.camera
    const postprocessing = this.options.postprocessing
    const sphereMesh = this.sphereMesh
    const orbitRadius = this.options.postprocessing.orbitRadius
    const clipPosition = this.clipPosition
    const sunPosition = this.sunPosition
    const screenSpacePosition = this.screenSpacePosition
    const materialDepth = this.materialDepth
    const time = Date.now() / 4000;

    sphereMesh.position.x = orbitRadius * Math.cos(time);
    sphereMesh.position.z = orbitRadius * Math.sin(time) - 100;

    if (postprocessing.enabled) {

      clipPosition.x = sunPosition.x;
      clipPosition.y = sunPosition.y;
      clipPosition.z = sunPosition.z;
      clipPosition.w = 1;

      clipPosition.applyMatrix4(camera.matrixWorldInverse).applyMatrix4(camera.projectionMatrix);

      // perspective divide (produce NDC space)

      clipPosition.x /= clipPosition.w;
      clipPosition.y /= clipPosition.w;

      screenSpacePosition.x = (clipPosition.x + 1) / 2; // transform from [-1,1] to [0,1]
      screenSpacePosition.y = (clipPosition.y + 1) / 2; // transform from [-1,1] to [0,1]
      screenSpacePosition.z = clipPosition.z; // needs to stay in clip space for visibilty checks

      // Give it to the god-ray and sun shaders

      postprocessing.godrayGenUniforms["vSunPositionScreenSpace"].value.copy(screenSpacePosition);
      postprocessing.godraysFakeSunUniforms["vSunPositionScreenSpace"].value.copy(screenSpacePosition);

      // -- Draw sky and sun --

      // Clear colors and depths, will clear to sky color

      renderer.setRenderTarget(postprocessing.rtTextureColors);
      renderer.clear(true, true, false);

      // Sun render. Runs a shader that gives a brightness based on the screen
      // space distance to the sun. Not very efficient, so i make a scissor
      // rectangle around the suns position to avoid rendering surrounding pixels.

      const sunsqH = 0.74 * window.innerHeight; // 0.74 depends on extent of sun from shader
      const sunsqW = 0.74 * window.innerHeight; // both depend on height because sun is aspect-corrected

      screenSpacePosition.x *= window.innerWidth;
      screenSpacePosition.y *= window.innerHeight;

      renderer.setScissor(screenSpacePosition.x - sunsqW / 2, screenSpacePosition.y - sunsqH / 2, sunsqW, sunsqH);
      renderer.setScissorTest(true);

      postprocessing.godraysFakeSunUniforms["fAspect"].value = window.innerWidth / window.innerHeight;

      postprocessing.scene.overrideMaterial = postprocessing.materialGodraysFakeSun;
      renderer.setRenderTarget(postprocessing.rtTextureColors);
      renderer.render(postprocessing.scene, postprocessing.camera);

      renderer.setScissorTest(false);

      // -- Draw scene objects --

      // Colors

      scene.overrideMaterial = null;
      renderer.setRenderTarget(postprocessing.rtTextureColors);
      renderer.render(scene, camera);

      // Depth

      scene.overrideMaterial = materialDepth;
      renderer.setRenderTarget(postprocessing.rtTextureDepth);
      renderer.clear();
      renderer.render(scene, camera);

      //

      postprocessing.godrayMaskUniforms["tInput"].value = postprocessing.rtTextureDepth.texture;

      postprocessing.scene.overrideMaterial = postprocessing.materialGodraysDepthMask;
      renderer.setRenderTarget(postprocessing.rtTextureDepthMask);
      renderer.render(postprocessing.scene, postprocessing.camera);

      // -- Render god-rays --

      // Maximum length of god-rays (in texture space [0,1]X[0,1])

      const filterLen = 1.0;

      // Samples taken by filter

      const TAPS_PER_PASS = 6.0;

      // Pass order could equivalently be 3,2,1 (instead of 1,2,3), which
      // would start with a small filter support and grow to large. however
      // the large-to-small order produces less objectionable aliasing artifacts that
      // appear as a glimmer along the length of the beams

      // pass 1 - render into first ping-pong target
      filterGodRays(postprocessing.rtTextureDepthMask.texture, postprocessing.rtTextureGodRays2, getStepSize(filterLen, TAPS_PER_PASS, 1.0), postprocessing, renderer);

      // pass 2 - render into second ping-pong target
      filterGodRays(postprocessing.rtTextureGodRays2.texture, postprocessing.rtTextureGodRays1, getStepSize(filterLen, TAPS_PER_PASS, 2.0), postprocessing, renderer);

      // pass 3 - 1st RT
      filterGodRays(postprocessing.rtTextureGodRays1.texture, postprocessing.rtTextureGodRays2, getStepSize(filterLen, TAPS_PER_PASS, 3.0), postprocessing, renderer);

      // final pass - composite god-rays onto colors

      postprocessing.godrayCombineUniforms["tColors"].value = postprocessing.rtTextureColors.texture;
      postprocessing.godrayCombineUniforms["tGodRays"].value = postprocessing.rtTextureGodRays2.texture;

      postprocessing.scene.overrideMaterial = postprocessing.materialGodraysCombine;

      renderer.setRenderTarget(null);
      renderer.render(postprocessing.scene, postprocessing.camera);
      postprocessing.scene.overrideMaterial = null;

    } else {

      renderer.setRenderTarget(null);
      renderer.clear();
      renderer.render(scene, camera);

    }
  }

  initControl() {
    let controls = new OrbitControls(this.camera, this.renderer.domElement)
    controls.enableDamping = true
    controls.dampingFactor = 0.25
    controls.screenSpacePanning = false
    // controls.maxDistance = 800
    controls.enableZoom = true;      //是否可以缩放
    // controls.autoRotate = true;   //是否自动旋转
    controls.autoRotateSpeed = 0.3;
    controls.minDistance = 1;        //设置相机距离原点的最远距离
    controls.maxDistance = 1000;     //设置相机距离原点的最远距离
    controls.update()
    this.controls = controls
  }

  initCamera() {
    let k = this.options.width / this.options.height
    let camera = new THREE.PerspectiveCamera(70, k, 1, 1000)
    camera.position.set(0, 0, 200)
    this.camera = camera
    this.scene.add(this.camera)
    this.camera.lookAt(this.scene.position)
  }

  initLight() {
    let point = new THREE.PointLight(0xffffff)
    point.position.set(400, 200, 300)
    let ambient = new THREE.AmbientLight(0x444444)
    this.light = [point, ambient]
    this.scene.add(point)
    this.scene.add(ambient)
  }

  initGeo() {
    this.materialDepth = new THREE.MeshDepthMaterial();
    const geo = new THREE.SphereGeometry(1, 20, 10)
    const materialScene = new THREE.MeshBasicMaterial({ color: 0x000000 })
    const sphereMesh = new THREE.Mesh(geo, materialScene)
    sphereMesh.scale.multiplyScalar(20)
    this.scene.add(sphereMesh)
    this.sphereMesh = sphereMesh
  }

  initLoader() {
    var _self = this
    const loader = new OBJLoader()
    // loader.load('/static/models/bracelet/vers(4)-men-design #RGmen_US 5_H 1.65_W 5.obj', function (object) {
    loader.load('/static/models/tree/tree.obj', function (object) {
      var material = new THREE.MeshLambertMaterial({
        color: 0x000000
      })
      object.material = material;
      object.position.set(0, - 150, - 150);
      object.scale.multiplyScalar(400);
      _self.scene.add(object)
    })
  }

  drawGridHelper() {
    let gridHelper = new THREE.GridHelper(400, 160, new THREE.Color(0x555555), new THREE.Color(0x333333))
    this.gridHelper = gridHelper
    this.scene.add(this.gridHelper)
  }

  initScene() {
    this.scene = new THREE.Scene({
      background: this.options.scene.background
    })
  }

  init() {
    let renderer = new THREE.WebGLRenderer()

    renderer.setPixelRatio(window.devicePixelRatio)
    renderer.setSize(window.innerWidth, window.innerHeight)
    renderer.setClearColor(this.options.renderer.clearColor, 1)

    this.el.appendChild(renderer.domElement)
    this.renderer = renderer
  }

  initClock() {
    this.clock = new THREE.Clock()
  }

  initStat() {
    let stats = new Stats()
    this.el.appendChild(stats.domElement)
    this.stats = stats
  }
}

function filterGodRays(inputTex, renderTarget, stepSize, postprocessing, renderer) {

  postprocessing.scene.overrideMaterial = postprocessing.materialGodraysGenerate;

  postprocessing.godrayGenUniforms["fStepSize"].value = stepSize;
  postprocessing.godrayGenUniforms["tInput"].value = inputTex;

  renderer.setRenderTarget(renderTarget);
  renderer.render(postprocessing.scene, postprocessing.camera);
  postprocessing.scene.overrideMaterial = null;

}

function getStepSize(filterLen, tapsPerPass, pass) {

  return filterLen * Math.pow(tapsPerPass, - pass);

}

export {
  Tree
}
